/*
 * Copyright (C) 2010 Fred Barrie This program is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version. This program is distributed
 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details. You should have received
 * a copy of the GNU General Public License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.aergia.vinny.engine.servicetime;

import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import org.aergia.vinny.core.ApiResult;
import org.aergia.vinny.database.Database;
import org.aergia.vinny.database.pojo.Card;
import org.aergia.vinny.database.pojo.Scan;
import org.aergia.vinny.database.pojo.Service;
import org.aergia.vinny.database.pojo.VinnyOption;
import org.aergia.vinny.database.pojo.VinnyUser;
import org.aergia.vinny.util.DateUtil;

/**
 * @author fred
 */
public abstract class ServiceTimeCalculatorSkeleton implements ServiceTimeCalculator {

	private static final int DEFAULT_MINIMUM_SERVICE_TIME = 5;
	protected final Database database;
	private final VinnyUser admin;
	private final boolean autoAuthorize;
	private final Logger logger;
	private final int minimumServiceTime;

	ServiceTimeCalculatorSkeleton(final Database database, final Logger logger) {
		this.logger = logger;
		this.database = database;
		this.admin = this.getAdminUser();
		this.autoAuthorize = this.getAutoAuthorize();
		this.minimumServiceTime = this.getMinimumScanTime();
	}

	@Override
	public void calculateService() {
		final Collection<Scan> scans = this.database.getUnmatchedScans();
		final Set<Scan> workArea = new TreeSet<Scan>();
		final Map<Card, Scan> volunteerStartTime = new HashMap<Card, Scan>();
		for (final Scan scan : scans) {
			workArea.add(scan);
		}

		for (final Scan scan : workArea) {

			if (volunteerStartTime.containsKey(scan.getCard())) {
				final Scan previous = volunteerStartTime.get(scan.getCard());
				final long delta = DateUtil.getDifference(previous.getScanTime(), scan.getScanTime());

				if (delta < this.minimumServiceTime) {
					scan.setActive(false);
					scan.setComment("Scan too fast");
					this.database.saveScan(scan);
					continue;
				}

				if (this.isScanTimesOk(previous, scan)) {
					final Service service = new Service(previous, scan);
					if (this.isAutoAuthoriable(service)) {
						service.setApproved(true);
						service.setApprovedBy(this.admin);
					}
					this.saveServiceScans(service, previous, scan);
					volunteerStartTime.remove(previous.getCard());
				} else {
					final Calendar endTime = this.getAppropriateEndTime(previous);
					final Service service = new Service(previous, endTime, "Service stopped by system.");
					this.saveServiceScans(service, previous, null);
					volunteerStartTime.remove(previous.getCard());
					volunteerStartTime.put(scan.getCard(), scan);
				}
			} else {
				volunteerStartTime.put(scan.getCard(), scan);
			}
		}

		for (final Scan scan : volunteerStartTime.values()) {
			if (this.isScanOutOfRange(scan)) {
				final Calendar endTime = this.getAppropriateEndTime(scan);
				final Service service = new Service(scan, endTime, "Service stopped by system.");
				this.saveServiceScans(service, scan, null);
			}
		}
	}

	abstract protected Calendar getAppropriateEndTime(Scan scan);

	abstract protected boolean isInServiceTime(Service service);

	abstract protected boolean isScanOutOfRange(Scan scan);

	abstract protected boolean isScanTimesOk(Scan startScan, Scan endScan);

	private VinnyUser getAdminUser() {
		final ApiResult<VinnyUser> result = this.database.getUser("admin");
		if (result.isError()) {
			this.logger.severe("There is no admin user.");
			return null;
		}
		return result.getResult();
	}

	private boolean getAutoAuthorize() {
		boolean result = false;
		final ApiResult<VinnyOption> optionResult = this.database.getOption(VinnyOption.AUTO_AUTHORIZE);
		if (!optionResult.isError()) {
			result = Boolean.parseBoolean(optionResult.getResult().getValue());
		}
		return result;
	}

	private int getMinimumScanTime() {
		int result = ServiceTimeCalculatorSkeleton.DEFAULT_MINIMUM_SERVICE_TIME;
		final ApiResult<VinnyOption> optionResult = this.database.getOption(VinnyOption.MINIMUM_SERVICE_TIME);
		if (!optionResult.isError()) {
			try {
				result = Integer.parseInt(optionResult.getResult().getValue());
			} catch (final NumberFormatException nfe) {
				// ignore
			}
		}
		return result;
	}

	private boolean isAutoAuthoriable(final Service service) {
		if (this.autoAuthorize && this.isInServiceTime(service)) {
			return true;
		}
		return false;
	}

	private void saveServiceScans(final Service service, final Scan scan1, final Scan scan2) {
		this.database.addService(service);
		scan1.setService(service);
		this.database.saveScan(scan1);
		if (scan2 != null) {
			scan2.setService(service);
			this.database.saveScan(scan2);
		}
	}
}
